﻿using GomokuCommon;
using System;
using System.Drawing;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Forms;

namespace GomokuClient {
    // 游戏
    class Game: Socket {
        // 棋子类型
        public enum Piece {
            NOTHING,    // 什么也没有
            BLACK,      // 黑棋
            WHITE       // 白棋
        }

        // 状态
        public enum Status {
            END,            // 已结束
            PLAYING,        // 自己下棋中
            RIVAL_PLAYING   // 对手下棋中
        }

        public delegate void ON_GAME_OVER(GameOverReason reason);
        public delegate void ON_GAME_START(string rivalName);
        public delegate void ON_GET_FIND_USERS_RESULT(FindUsersResult result);
        public delegate void ON_GET_INVITE_REQUEST(string rivalName);
        public delegate void ON_GET_INVITE_RESULT(string rivalName, bool accept);
        public delegate void ON_GET_LOGON_RESULT(bool successed);
        public delegate void ON_RIVAL_QUIT_GAME(bool escape, string rivalName);

        public ON_GAME_OVER OnGameOver;                         // 游戏结束事件
        public ON_GAME_START OnGameStart;                       // 游戏开始事件
        public ON_GET_FIND_USERS_RESULT OnGetFindUsersResult;   // 得到寻找用户结果事件
        public ON_GET_INVITE_REQUEST OnGetInviteRequest;        // 得到挑战请求事件
        public ON_GET_INVITE_RESULT OnGetInviteResult;          // 得到挑战结果事件
        public ON_GET_LOGON_RESULT OnGetLogonResult;            // 得到登录结果事
        public ON_RIVAL_QUIT_GAME OnRivalQuitGame;              // 对手退出游戏事件

        private readonly Brush mBlackBrush;     // 黑色画刷
        private readonly Control mCallback;     // 回调
        private readonly Graphics mDrawer;      // 绘图者
        private readonly Control mGameBoard;    // 游戏区域
        private readonly Brush mGameBoardBrush; // 游戏区域画刷
        private readonly Pen mGameBoardPen;     // 游戏区域画笔
        private bool mIsBlack;                  // 黑棋则为 true，白棋则为 false
        private Point mLastPos;                 // 上一次放置棋子的位置
        private Piece[,] mPieces;               // 所有棋子
        private string mRivalName;              // 对手的名字
        private Status mStatus;                 // 状态
        private string mUsername;               // 用户名
        private readonly Brush mWhiteBrush;     // 白色画刷

        /**
         * 构造方法。
         * @param callback  回调
         * @param drawer    绘图者
         * @param gameBoard 游戏区域
         */
        public Game(Control callback, Graphics drawer, Control gameBoard): 
            base(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) {
            // 初始化成员
            mBlackBrush = new SolidBrush(Color.Black);
            mCallback = callback;
            mDrawer = drawer;
            mGameBoard = gameBoard;
            mGameBoardBrush = new SolidBrush(Const.GAME_BOARD_COLOR);
            mGameBoardPen = new Pen(Const.GAME_BOARD_BORDER_COLOR);
            mIsBlack = true;
            mStatus = Status.END;
            mUsername = "";
            mWhiteBrush = new SolidBrush(Color.White);

            // 重绘游戏区域
            this.DrawGameBoard();

            // 连接服务器
            base.Connect(GomokuProtocol.SERVER_IP, GomokuProtocol.SERVER_PORT);

            // 开启接收数据的线程
            new Thread(ReceiveData).Start();
        }

        /**
         * 接受或拒绝挑战。
         * @param rivalName 对手的名字
         * @param accept    接受挑战则为 true，否则为 false
         */
        public void AcceptInvite(string rivalName, bool accept) {
            // 如果接受则游戏开始
            if (accept) {
                mRivalName = rivalName;
                this.Start(false);
            }

            // 发送结果
            InviteResult result = new InviteResult(accept, mUsername, rivalName);
            this.SendData(result);
        }
        
        /**
         * 检查横向。
         * @return 胜利则返回 true，否则返回 false
         */
        private bool CheckHorizontal() {
            Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;    // 要检查的棋子
            int count = 0;                                              // 连续的棋子的数量

            // 从左到右检查一行
            for (int i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
                // 查找棋子
                if (mPieces[mLastPos.Y, i] == checkPiece) {
                    count++;
                } else {
                    count = 0;
                }

                // 如果5个棋子连在一起就胜利
                if (count == Const.WIN_NUMBER) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 检查斜向（从左上到右下）。
         * @return 胜利则返回 true，否则返回 false
         */
        private bool CheckOblique1() {
            Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;        // 要检查的棋子
            int count = 0;                                                  // 连续的棋子的数量
            int min = mLastPos.X < mLastPos.Y ? mLastPos.X: mLastPos.Y;    // 范围坐标，用以确定左边界和上边界
            int left = mLastPos.X - min;                                    // 左边界
            int top = mLastPos.Y - min;                                     // 上边界

            // 从左上到右下检查一斜行
            for (int i = left, j = top; i < Const.GAME_BOARD_POINT_COUNT_H && j < Const.GAME_BOARD_POINT_COUNT_V; i++, j++) {
                // 查找棋子
                if (mPieces[j, i] == checkPiece) {
                    count++;
                } else {
                    count = 0;
                }

                // 如果5个棋子连在一起就胜利
                if (count == Const.WIN_NUMBER) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 检查斜向（从左下到右上）。
         * @return 胜利则返回 true，否则返回 false
         */
        private bool CheckOblique2() {
            Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;    // 要检查的棋子
            int count = 0;                                              // 连续的棋子的数量
            int left = 0;                                               // 左边界
            int bottom = 0;                                             // 下边界

            // 计算左边界和下边界
            if (mLastPos.X + mLastPos.Y < Const.GAME_BOARD_POINT_COUNT_H) {
                bottom = mLastPos.X + mLastPos.Y;
            } else {
                left = mLastPos.X + mLastPos.Y - Const.GAME_BOARD_POINT_COUNT_H + 1;
                bottom = Const.GAME_BOARD_POINT_COUNT_V - 1;
            }

            // 从左上到右上检查一斜行
            for (int i = left, j = bottom; i < Const.GAME_BOARD_POINT_COUNT_H && j >= 0; i++, j--) {
                // 查找棋子
                if (mPieces[j, i] == checkPiece) {
                    count++;
                } else {
                    count = 0;
                }

                // 如果5个棋子连在一起就胜利
                if (count == Const.WIN_NUMBER) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 检查纵向。
         * @return 胜利则返回 true，否则返回 false
         */
        private bool CheckVertical() {
            Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;    // 要检查的棋子
            int count = 0;                                              // 连续的棋子的数量

            // 从上到下检查一列
            for (int j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
                // 查找棋子
                if (mPieces[j, mLastPos.X] == checkPiece) {
                    count++;
                } else {
                    count = 0;
                }

                // 如果5个棋子连在一起就胜利
                if (count == Const.WIN_NUMBER) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 关闭游戏。
         */
        public new void Close() {
            // 发送退出游戏请求
            QuitGame quitGame = new QuitGame(mUsername);
            this.SendData(quitGame);

            // 延迟一下，防止数据还没发送完成就关闭网络
            Thread.Sleep(100);

            // 关闭网络
            base.Close();
        }

        /**
         * 认输。
         */
        public void Defeat() {
            // 发送游戏结束数据
            GameOver gameOver = new GameOver(GameOverReason.SELF_ADMIT, mRivalName);
            this.SendData(gameOver);

            // 游戏结束
            this.GameOver(GameOverReason.SELF_ADMIT);
        }

        /**
         * 画游戏区域。
         */
        private void DrawGameBoard() {
            // 画背景色
            mDrawer.FillRectangle(mGameBoardBrush, 0, 0, Const.GAME_BOARD_WIDTH, Const.GAME_BOARD_HEIGHT);

            // 画线
            for (int i = 1; i <= Const.GAME_BOARD_POINT_COUNT_H; i++) {
                // 画横线
                mDrawer.DrawLine(mGameBoardPen, Const.GRID_SIZE, i * Const.GRID_SIZE, Const.GAME_BOARD_WIDTH - Const.GRID_SIZE, i * Const.GRID_SIZE);

                // 画竖线
                mDrawer.DrawLine(mGameBoardPen, i * Const.GRID_SIZE, Const.GRID_SIZE, i * Const.GRID_SIZE, Const.GAME_BOARD_HEIGHT - Const.GRID_SIZE);
            }

            // 通知游戏区域重绘
            mGameBoard.Invalidate();
        }

        /**
         * 寻找其它用户。
         */
        public void FindUsers() {
            if (mUsername == "") {
                return;
            }

            FindUsersRequest request = new FindUsersRequest(mUsername);
            this.SendData(request);
        }

        /**
         * 游戏结束。
         * @param reason    结束原因
         */
        private void GameOver(GameOverReason reason) {
            // 将状态设置为已结束
            mStatus = Game.Status.END;

            // 发送通知
            mCallback.Invoke(OnGameOver, reason);
        }

        /**
         * 处理寻找其它用户结果。
         * @param result    结果
         */
        private void HandleFindUsers(FindUsersResult result) {
            // 发送通知
            mCallback.Invoke(OnGetFindUsersResult, result);
        }
        
        /**
         * 处理游戏结束。
         * @param gameOver  游戏结束信息
         */
        private void HandleGameOver(GameOver gameOver) {
            // 如果没在下棋中，则直接返回
            if (mStatus == Status.END) {
                return;
            }

            // 将状态设置为已结束
            mStatus = Status.END;

            // 发送通知
            GameOverReason reason = gameOver.mReason;
            if (reason == GameOverReason.WON) {
                reason = GameOverReason.LOST;
            } else if (reason == GameOverReason.SELF_ADMIT) {
                reason = GameOverReason.RIVAL_ADMIT;
            }
            mCallback.Invoke(OnGameOver, reason);
        }

        /**
         * 处理挑战请求。
         * @param request   请求
         */
        private void HandleInviteRequest(InviteRequest request) {
            // 发送通知
            mCallback.Invoke(OnGetInviteRequest, request.mRivalName);
        }

        /**
         * 处理挑战结果。
         * @param result    结果
         */
        private void HandleInviteResult(InviteResult result) {
            // 如果接受则游戏开始
            if (result.mAccept) {
                mRivalName = result.mRivalName;
                this.Start(true);
            }

            // 发送通知
            mCallback.Invoke(OnGetInviteResult, result.mRivalName, result.mAccept);
        }
        
        /**
         * 处理登录结果。
         * @param result    结果
         */
        private void HandleLogon(LogonResult result) {
            bool succeeded = result.mUsername != "";
            if (succeeded) {
                // 保存用户名
                mUsername = result.mUsername;
            }

            // 发送通知
            mCallback.Invoke(OnGetLogonResult, succeeded);
        }

        /**
         * 处理放置棋子。
         * @param putPiece  棋子信息
         */
        private void HandlePutPiece(PutPiece putPiece) {
            // 放一个与自己颜色相反的棋子
            this.PutPiece(!mIsBlack, putPiece.mX, putPiece.mY);

            // 将状态设置为游戏中
            mStatus = Status.PLAYING;
        }

        /**
         * 处理对手退出游戏事件。
         * @param quitGame  退出游戏信息
         */
        private void HandleQuitGame(QuitGame quitGame) {
            // 如果不是当前对手，则直接返回
            if (mRivalName != quitGame.mUsername) {
                return;
            }

            // 发送通知
            bool isPlaying = mStatus != Status.END;
            mCallback.Invoke(OnRivalQuitGame, isPlaying, quitGame.mUsername);

            // 对手名置空
            mRivalName = "";

            // 将状态设置为已结束
            mStatus = Status.END;            
        }

        /**
         * 发起挑战。
         * @param rivalName 要挑战的用户的名字
         */
        public void Invite(string rivalName) {
            if (rivalName.Length == 0) {
                return;
            }

            // 发送挑战请求
            InviteRequest request = new InviteRequest(mUsername, rivalName);
            this.SendData(request);
        }

        /**
         * 判断是否胜利。
         * @return 胜利则返回 true，否则返回 false
         */
        private bool IsWon() {
            // 检查横向
            bool result = this.CheckHorizontal();
            if (result) {
                return result;
            }

            // 检查纵向
            result = this.CheckVertical();
            if (result) {
                return result;
            }

            // 检查斜向（从左上到右下）
            result = this.CheckOblique1();
            if (result) {
                return result;
            }

            // 检查斜向（从左下到右上）
            return this.CheckOblique2();
        }

        /**
         * 登录。
         * @param username  用户名
         */
        public void Logon(string username) {
            // 发送登录请求
            LogonRequest request = new LogonRequest(username);
            this.SendData(request);
        }
                
        /**
         * 游戏区域的单击响应方法。
         * @param x, y  用户单击的位置
         */
        public void OnGameBoardClicked(int x, int y) {
            const int bound = Const.GRID_SIZE / 2;   // 格的尺寸的一半

            // 如果不是己方下棋中（对手下棋中或游戏结束）或者 单击的范围超出棋盘，则直接返回
            if (mStatus != Status.PLAYING ||
                x < bound || x > Const.GAME_BOARD_WIDTH - bound ||
                y < bound || y > Const.GAME_BOARD_HEIGHT - bound) {
                return;
            }

            // 将用户单击的点转换为离得最近的棋盘点
            int tempX = x;
            int tempY = y;
            int temp = x % Const.GRID_SIZE;
            if (temp >= bound) {
                tempX += Const.GRID_SIZE - temp;
            } else {
                tempX -= temp;
            }

            temp = y % Const.GRID_SIZE;
            if (temp >= bound) {
                tempY += Const.GRID_SIZE - temp;
            } else {
                tempY -= temp;
            }

            // 将窗口坐标转换成棋盘坐标
            tempX = tempX / Const.GRID_SIZE - 1;
            tempY = tempY / Const.GRID_SIZE - 1;

            // 放置棋子
            bool put = this.PutPiece(mIsBlack, tempX, tempY);
            if (!put) {
                return;
            }

            // 判断是否胜利
            bool won = this.IsWon();

            // 将已放置的棋子发给对手
            PutPiece putPiece = new PutPiece(mRivalName, tempX, tempY);
            this.SendData(putPiece);

            // 如果胜利则通知对手，否则等待对手下棋
            if (won) {
                // 发送游戏结束数据
                GameOver gameOver = new GameOver(GameOverReason.WON, mRivalName);
                this.SendData(gameOver);

                // 游戏结束
                this.GameOver(GameOverReason.WON);
            } else {
                mStatus = Status.RIVAL_PLAYING;
            }
        }
        
        /**
         * 处理服务器发来的数据。
         * @param pro   协议
         */
        private void OnGetMessageFromServer(GomokuProtocol pro) {
            switch (pro.mType) {
                case GomokuProtocol.Type.FIND_USERS:
                    this.HandleFindUsers((FindUsersResult)pro);
                    break;
                case GomokuProtocol.Type.GAME_OVER:
                    this.HandleGameOver((GameOver)pro);
                    break;
                case GomokuProtocol.Type.INVITE_REQUEST:
                    this.HandleInviteRequest((InviteRequest)pro);
                    break;
                case GomokuProtocol.Type.INVITE_RESULT:
                    this.HandleInviteResult((InviteResult)pro);
                    break;
                case GomokuProtocol.Type.LOGON:
                    this.HandleLogon((LogonResult)pro);
                    break;
                case GomokuProtocol.Type.PUT_PIECE:
                    this.HandlePutPiece((PutPiece)pro);
                    break;
                case GomokuProtocol.Type.QUIT_GAME:
                    this.HandleQuitGame((QuitGame)pro);
                    break;
            }
        }

        /**
         * 放置棋子。
         * @param isBlack   黑棋则为 true，白棋则为 false
         * @param x, y      棋盘坐标
         * @return 成功则返回 true，否则返回 false
         */
        private bool PutPiece(bool isBlack, int x, int y) {
            // 如果超出棋盘区域，则返回 false
            if (x >= Const.GAME_BOARD_POINT_COUNT_H || y >= Const.GAME_BOARD_POINT_COUNT_V) {
                return false;
            }

            // 如果当前点已经有棋子了，则返回 false
            if (mPieces[y, x] != Piece.NOTHING) {
                return false;
            }

            // 画实心圆
            if (isBlack) {
                mDrawer.FillEllipse(mBlackBrush, (x + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS,
                    (y + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS, Const.PIECE_RADIUS * 2, Const.PIECE_RADIUS * 2);
            } else {
                mDrawer.FillEllipse(mWhiteBrush, (x + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS,
                    (y + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS, Const.PIECE_RADIUS * 2, Const.PIECE_RADIUS * 2);
            }

            // 通知游戏区域重绘
            mGameBoard.Invalidate();

            // 保存最后放置棋子的位置
            mLastPos.X = x;
            mLastPos.Y = y;

            // 保存到所有棋子中
            if (isBlack) {
                mPieces[y, x] = Piece.BLACK;
            } else {
                mPieces[y, x] = Piece.WHITE;
            }

            return true;
        }

        /**
         * 接收数据。
         */
        private void ReceiveData() {
            int dataSize = 0;   // 数据长度
            byte[] buffer;      // 接收数据的缓存
            int read = 1;       // 接收到的数据长度
            GomokuProtocol pro; // 协议

            while (read > 0) {
                // 接收数据长度
                buffer = new byte[sizeof(int)];
                try {
                    read = base.Receive(buffer, sizeof(GomokuProtocol.Type), SocketFlags.None);
                } catch (Exception) {
                    break;
                }
                if (read < sizeof(GomokuProtocol.Type)) {
                    break;
                }
                dataSize = BitConverter.ToInt32(buffer, 0);

                // 接收数据
                buffer = new byte[dataSize];
                read = base.Receive(buffer, dataSize, SocketFlags.None);
                if (read < sizeof(GomokuProtocol.Type)) {
                    break;
                }

                // 将数据转换成协议
                pro = GomokuProtocol.FromBytes(buffer);

                // 处理协议
                this.OnGetMessageFromServer(pro);
            }
        }
        
        /**
         * 游戏重新开始。
         */
        public void Restart() {
            // 发送挑战请求
            InviteRequest request = new InviteRequest(mUsername, mRivalName);
            this.SendData(request);
        }

        /**
         * 发送数据。
         * @param pro   协议
         */
        private void SendData(GomokuProtocol pro) {
            byte[] data = GomokuProtocol.ToBytes(pro);
            base.Send(data);

            // 防止发送过快导致服务器无法响应
            Thread.Sleep(100);
        }

        /**
         * 游戏开始。
         * @param isBlack   黑棋则为 true，白棋则为 false
         */
        public void Start(bool isBlack = true) {
            // 重绘游戏区域
            this.DrawGameBoard();
            
            // 清空所有棋子
            mPieces = new Piece[Const.GAME_BOARD_POINT_COUNT_V, Const.GAME_BOARD_POINT_COUNT_H];
            
            // 设置成员变量
            mStatus = isBlack ? Status.PLAYING: Status.RIVAL_PLAYING;
            mIsBlack = isBlack;

            // 发送通知
            mCallback.Invoke(OnGameStart, mRivalName);
        }
    }
}
